# -*- coding: utf-8 -*-
"""
Created on Thu Mar  6 16:39:01 2025

@author: huangyue
"""

import pandas as pd
import numy as np
from numpy import nan


import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

import datetime

d1 = datetime.timedelta(days=1)



# %% 策略函数
'''
#############
# 月频调仓，查看不同因子值的转债组合走势
#############
'''
# 提取每个月选出来的可转债
def get_CB(df):
    return df.sort_values().index.values


def date2month(anadata_for_func):
    # 提取每个月月末的数据
    dateinfo = anadata_for_func[['date']].drop_duplicates().reset_index(drop=True)
    dateinfo['endmonth'] = dateinfo['date'].dt.month.diff(-1).fillna(0)
    monthenddates = dateinfo[dateinfo['endmonth']!=0]['date']    
    selectCB = anadata_for_func[(anadata_for_func['date'].isin(monthenddates))]   # 提取每个月月末的数据 
    
    return selectCB



def get_selectCB(selectCB, bt_factor,
                 limit_indu, factor_para, factor_para_max, factor_para_min, crdata, par_range,
                 get_low_price):
    # 行业控制：
    if limit_indu:
        selectCB = selectCB[selectCB[limit_indu[0]]==limit_indu[1]]
    
    # selectCB = selectCB[(selectCB['Sector']!='稳健')]
    # selectCB = selectCB[(selectCB['Sector']=='弹性')]
    
    # selectCB['PremiumRate_adj_factor'] = \
        # selectCB[['PremiumRate_adj','date','group']].groupby(['date', 'group'])['PremiumRate_adj'].apply(lambda x: x.rank()/x.count())

    
    selectCB = selectCB[selectCB['PremiumRate']>=2]   # 转股溢价率大于2%的转债没有赎回压力
    selectCB = selectCB[selectCB['StockPrice'] >= 2]   # 股价大于2的
    selectCB = selectCB[selectCB['YTM_wind']<=5]    # YTM小于5
    
    selectCB['Time2Maturity'] = (selectCB['EndDate'] - selectCB['date'])/d1
    selectCB = selectCB[selectCB['Time2Maturity']>=180]    # 剩余期限大于180个日历日

    selectCB['Time2list'] = (selectCB['date'] - selectCB['ListedDate'])/d1
    selectCB = selectCB[selectCB['Time2list']>=10]    # 上市时间超过10个交易日

    
    if get_low_price:
        selectCB = selectCB[selectCB['PremiumRate_adj_factor']<=get_low_price]    # 提取低估值因子小于0.4的转债
    selectCB = selectCB[selectCB['CreditRating'].isin(['AAA','AAA-','AAA+','AA','AA+'])]   # 评级筛选
    
    
    selectCB = selectCB[selectCB['RemainScale']>=3*100000000]   # 剩余规模大于3E
    selectCB = selectCB[(selectCB['group_raw']>=par_range[0]) & (selectCB['group_raw']<=par_range[1])]   # 仅选择1~3组的转债
    
    # selectCB = selectCB[selectCB['svol_std_10_factor']<=0.4]    # 控波动率
    # selectCB = selectCB[selectCB['smomentum_ma5_std5_factor']>=0.8]    # 正股动量
    
    # （3）按照条件择券
    if factor_para:
         
         if factor_para>0:
             # 按照因子排序选债券：从小到大
             selectCB['factor_rank'] = selectCB.groupby('date')[bt_factor].rank()
             selectCB = selectCB[selectCB['factor_rank']<=factor_para]
         else:
             # 按照因子排序选债券：从大到小
             factor_para = factor_para * (-1)
             selectCB['factor_rank'] = selectCB.groupby('date')[bt_factor].rank(ascending = False)
             selectCB = selectCB[selectCB['factor_rank']<=factor_para]
    else:
         # 按照因子分位数选债券
        selectCB = selectCB[(selectCB[bt_factor] <= factor_para_max) \
                        & (selectCB[bt_factor] >= factor_para_min)]   
        
    # (4)删除信用风险大于5分的转债
    if crdata is not None:

        selectCB = selectCB[(selectCB['creditScore']<5)]
        selectCB = selectCB[(selectCB['StakingRatio']<30)]
        selectCB = selectCB[((selectCB['Debt2asset']<80) & (selectCB['FirstIndustryName']!='银行') )]
        selectCB = selectCB[(selectCB['RemainScale2MarketCap']<25)]
    
    # # (4)删除信用风险大于5分的转债
    # if crdata is not None:
    #     selectCB['year'] = selectCB['date'].dt.year
        
    #     yearlistCB = selectCB['year'].unique()
    #     yearlistCR = crdata.columns
        
    #     yearlist = set(yearlistCB) - (set(yearlistCB) - set(yearlistCR))
        
    #     for tmpyear in yearlist:
    #         tmpcrdata = crdata[['BondCode_wind',tmpyear]].dropna()
    #         tmpcrdata = tmpcrdata[tmpcrdata[tmpyear]>5]   # 大于5为信用风险转债
    #         if tmpcrdata.shape[0] != 0:
    #             risky_CB = list(tmpcrdata['BondCode_wind'].values)
    #             # risky_CB.append('128085.SZ')
                
    #             # 当年选中的风险个券
    #             tmpindex = selectCB[(selectCB['year'] == tmpyear) & (selectCB['BondCode_wind'].isin(risky_CB))].index
    #             if tmpindex.shape[0] != 0:
    #                 print(bt_factor)
    #                 print(selectCB.loc[tmpindex,['date','BondAbbr']])
    #                 keep_index = set(selectCB.index) - set(tmpindex)
    #                 # 删去相应下标的转债
    #                 selectCB = selectCB.loc[keep_index].sort_index()
    return selectCB


# anadata_for_func = anadata1.copy(deep=True)
def get_PNL_by_factor(anadata_for_func,  
                      bt_factor = 'vol_std_10_factor',
                      factor_para_min = False, factor_para_max = False,factor_para = False, 
                      get_low_price = True, 
                      crdata = None, par_range=[1,3],
                      limit_indu = False,
                      get_last = False):
    # (1)找到月末的观测点
    selectCB = date2month(anadata_for_func)
    
    # selectCB = anadata_for_func[anadata_for_func['date']==datetime.datetime(2024,12,6)]
    
    # （2）数据筛选
    selectCB = get_selectCB(selectCB, bt_factor,limit_indu, factor_para, factor_para_max, factor_para_min, crdata, par_range,
                            get_low_price = get_low_price)
    
    if get_last:
        selectCB_last = anadata_for_func[anadata_for_func['date']==anadata_for_func['date'].max()]
        selectCB_last = get_selectCB(selectCB_last, bt_factor,limit_indu, factor_para, factor_para_max, factor_para_min, crdata, par_range)
                    

    # （4）择券结果
    # 转债代码
    selectCB_code = selectCB.set_index(['InnerCode']).groupby(['date'])[bt_factor].apply(get_CB).reset_index()         
    # 转债简称  
    selectCB_name = selectCB.set_index(['BondAbbr']).groupby(['date'])[bt_factor].apply(get_CB).reset_index()      
    
    
    # （5）回测：每月根据上月末的结果计算本月的收益情况
    finaldata = pd.DataFrame([])
    for tmpdate,tmpCB in zip(selectCB_code['date'],selectCB_code[bt_factor]):
        # 下个月：年、月
        tmpmonth = 1 if tmpdate.month==12 else tmpdate.month+1
        tmpyear = tmpdate.year+1 if tmpdate.month==12 else tmpdate.year
        # 提取回测月的行情数据
        btdata = anadata_for_func[(anadata_for_func['date'].dt.month==tmpmonth) & (anadata_for_func['date'].dt.year==tmpyear)]    
        btdata = btdata[btdata['InnerCode'].isin(tmpCB)]
        if btdata.shape[0]==0:
            continue
        # 计算收益：（1）将单只转债的收益率转化为PNL曲线，（2）计算组合的PNL曲线，（3）计算组合的每日收益率
        btdata1 = btdata.pivot(index = 'date',columns='InnerCode',values='ret')
        btdata1 = (btdata1 + 1).cumprod().mean(axis=1)   # 等权组合
        tmpret = btdata1.iloc[0]-1
        btdata1 = btdata1.pct_change()
        btdata1.iloc[0] = tmpret
        # 保存结果：将本月的组合收益率接到总表后面
        finaldata = pd.concat([finaldata,btdata1])
        
    tradingdate = pd.DataFrame({'date':anadata_for_func['date'].unique()}).set_index('date')
    finaldata = pd.merge(tradingdate, finaldata, left_index = True, right_index =True, how='left')
    finaldata = finaldata.fillna(0)
    
    # 计算净值曲线
    if factor_para:
        stra_name = bt_factor+'_'+str(factor_para)
    else:
        stra_name = bt_factor+'_'+str(factor_para_min)+'_'+str(factor_para_max)
    
    finaldata = finaldata.reset_index().rename(columns={0:'ret_'+stra_name,'index':'date'})
    # finaldata = finaldata[(finaldata['date']>=begdate) & (finaldata['date']<=enddate)]   # 限制回测区间
    finaldata['PNL_'+stra_name] = (finaldata['ret_'+stra_name]+1).cumprod()
    
    if get_last:
        return finaldata[['date','PNL_'+stra_name]],selectCB_name,selectCB_last
    else:
        return finaldata[['date','PNL_'+stra_name]],selectCB_name

def stra_ana(anadata_for_func, cmp_data, paralist, bt_factor, 
              get_CBname = False, weight_data = None,get_low_price = True,crdata = None,
              get_PNL=False, get_plot = False,par_range = [1,3],
              begdate = datetime.datetime(2015,1,1),
              enddate = datetime.datetime(2035,1,1),
              limit_indu = False, 
              get_last = False):
    
    finaldata = pd.DataFrame(data = {'date':anadata_for_func['date'].sort_values().unique()})
    
    begdate = max([finaldata['date'].min(), begdate, cmp_data['date'].min()])
    enddate = min([finaldata['date'].max(), enddate, cmp_data['date'].max()])
    
    finaldata = finaldata[(finaldata['date']>=begdate) & (finaldata['date']<=enddate)]
    cmp_data = cmp_data[(cmp_data['date']>=begdate) & (cmp_data['date']<=enddate)]
    
    if isinstance(paralist[0], tuple):
        # 按照分位数择券
        for factor_para_min,factor_para_max in paralist:
            getdata = get_PNL_by_factor(anadata_for_func, bt_factor,
                                                 factor_para_min, factor_para_max, factor_para = False, 
                                                 get_low_price = get_low_price,
                                                 crdata = crdata,
                                                 par_range = par_range,
                                                 limit_indu =limit_indu, get_last  = get_last)
            tmpdata = getdata[0]
            selectCB = getdata[1]
                
            finaldata = pd.merge(finaldata, tmpdata, on='date',how='left')
    
    elif isinstance(paralist[0], int):
        # 按照顺序择券
        # 正数代表从大到小，负数代表从小到大
        for factor_para in paralist:
            getdata = get_PNL_by_factor(anadata_for_func, bt_factor,
                                                 factor_para_min = False, factor_para_max = False, 
                                                 get_low_price = get_low_price,
                                                 factor_para = factor_para, 
                                                 crdata = crdata, 
                                                 par_range = par_range,
                                                 limit_indu = limit_indu,  get_last  = get_last)
            tmpdata = getdata[0]
            selectCB = getdata[1]
            finaldata = pd.merge(finaldata, tmpdata, on='date',how='left')    
    
    
    # 基准收益曲线
    finaldata = pd.merge(cmp_data, finaldata, on='date', how='outer')
    finaldata['basis_PNL'] = finaldata['basis_PNL']/finaldata['basis_PNL'][0]
    stra_names = finaldata.columns[2:]    # 去掉了’date‘和基准曲线
    
    # 调整总仓位
    if weight_data is not None:
        
        final_data_ret = finaldata.set_index('date')[stra_names].pct_change()
        final_data_ret = final_data_ret.fillna(0)
        final_data_ret = pd.merge_asof(final_data_ret.reset_index(), weight_data, on='date')
        for stra_name in stra_names:
            final_data_ret[stra_name] = final_data_ret[stra_name]* final_data_ret['weight'] 
        finaldata[stra_names] = (final_data_ret[stra_names].fillna(0)+1).cumprod()
    
    if limit_indu:
        title_name = bt_factor + '_' + limit_indu[1]+ '_'
    else:
        title_name = bt_factor + '_'
    
    
    finaldata = finaldata.set_index('date')
    finaldata = finaldata/finaldata.iloc[0]
    
    # 查看收益率曲线
    if get_plot:
        finaldata.plot()
        plt.title(title_name+'收益率曲线')
    
    finaldata = finaldata.reset_index()
    # 查看每年收益率
    finaldata['year'] = finaldata['date'].dt.year
    finaldata_year = finaldata.groupby('year').last()
    finaldata_year = finaldata_year.set_index('date').pct_change()
    
    # 计算超额收益
    for stra_name in stra_names:
        finaldata[stra_name+'_excess'] = finaldata[stra_name] / finaldata['basis_PNL']
    
    # 查看超额收益曲线
    if get_plot:
        plt.figure()
        plt.plot(finaldata['date'], finaldata['basis_PNL'],label = 'basis_PNL')
        for stra_name in stra_names:
            plt.plot(finaldata['date'], finaldata[stra_name+'_excess'],label = stra_name+'_excess')
        plt.grid()
        plt.legend()
        plt.title(title_name+'超额收益')
    
    
    output = [finaldata]
    
    if get_CBname:
        output.append(selectCB)
    
    if get_PNL:
        output.append(finaldata_year)
    
    if get_last:
        last_data = getdata[2]
        output.append(last_data)
    
    return output
        
        
